{
 "cells": [
  {
   "attachments": {},
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "# A quick tutorial\n",
    "\n",
    "This notebook provides a brief tutorial to modelling vehicle routing problems with PyVRP, introducing some of its most important modelling features:\n",
    "\n",
    "- We first solve a capacitated VRP, introducing the modelling interface and the most basic components.\n",
    "- We then solve a VRP with time windows, where we introduce the support PyVRP has for problems with duration constraints.\n",
    "- We then solve a multi-depot VRP with time windows and maximum route duration constraints.\n",
    "- We also solve a prize-collecting VRP with optional clients to showcase the modelling optional client visits.\n",
    "- We finally solve a VRP with simultaneous pickup and delivery to show problems with deliveries from the depot to clients, and return shipments from clients to depots."
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "### Capacitated VRP\n",
    "\n",
    "We will first model and solve the small capacitated VRP instance with 16 clients defined [in the OR-Tools documentation](https://developers.google.com/optimization/routing/cvrp).\n",
    "This instance has an optimal solution of cost 6208.\n",
    "The data are as follows:"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "# fmt: off\n",
    "COORDS = [\n",
    "    (456, 320),  # location 0 - the depot\n",
    "    (228, 0),    # location 1\n",
    "    (912, 0),    # location 2\n",
    "    (0, 80),     # location 3\n",
    "    (114, 80),   # location 4\n",
    "    (570, 160),  # location 5\n",
    "    (798, 160),  # location 6\n",
    "    (342, 240),  # location 7\n",
    "    (684, 240),  # location 8\n",
    "    (570, 400),  # location 9\n",
    "    (912, 400),  # location 10\n",
    "    (114, 480),  # location 11\n",
    "    (228, 480),  # location 12\n",
    "    (342, 560),  # location 13\n",
    "    (684, 560),  # location 14\n",
    "    (0, 640),    # location 15\n",
    "    (798, 640),  # location 16\n",
    "]\n",
    "DEMANDS = [0, 1, 1, 2, 4, 2, 4, 8, 8, 1, 2, 1, 2, 4, 4, 8, 8]\n",
    "# fmt: on"
   ]
  },
  {
   "attachments": {},
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "We can use the `pyvrp.Model` interface to conveniently specify our vehicle routing problem using this data.\n",
    "A full description of the `Model` interface is given in our [API documentation](https://pyvrp.org/api/pyvrp.html#pyvrp.Model.Model)."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "from pyvrp import Model\n",
    "\n",
    "m = Model()\n",
    "m.add_vehicle_type(4, capacity=15)\n",
    "depot = m.add_depot(x=COORDS[0][0], y=COORDS[0][1])\n",
    "clients = [\n",
    "    m.add_client(x=COORDS[idx][0], y=COORDS[idx][1], delivery=DEMANDS[idx])\n",
    "    for idx in range(1, len(COORDS))\n",
    "]\n",
    "\n",
    "locations = [depot] + clients\n",
    "for frm in locations:\n",
    "    for to in locations:\n",
    "        distance = abs(frm.x - to.x) + abs(frm.y - to.y)  # Manhattan\n",
    "        m.add_edge(frm, to, distance=distance)"
   ]
  },
  {
   "attachments": {},
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Let's inspect the resulting data instance."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "import matplotlib.pyplot as plt\n",
    "\n",
    "from pyvrp.plotting import plot_coordinates\n",
    "\n",
    "_, ax = plt.subplots(figsize=(8, 8))\n",
    "plot_coordinates(m.data(), ax=ax)"
   ]
  },
  {
   "attachments": {},
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "The instance looks good, so we are ready to solve it.\n",
    "Let's do so with a second of runtime, and display the search progress using the `display` argument on `Model.solve`."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "from pyvrp.stop import MaxRuntime\n",
    "\n",
    "res = m.solve(stop=MaxRuntime(1), display=True)  # one second"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "By passing the `display` argument, PyVRP displays statistics about the solver progress and the instance being solved.\n",
    "In particular, it outputs the sizes of the feasible and infeasible solution pools, their average objective values, and the objective of the best solutions in either pool.\n",
    "A heuristic improvement is indicated by a `H` at the start of a line.\n",
    "\n",
    "Let's print the solution we have found to see the routes."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "print(res)"
   ]
  },
  {
   "attachments": {},
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Good! Our solution attains the same objective value as the optimal solution OR-Tools finds.\n",
    "Let's inspect our solution more closely."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "from pyvrp.plotting import plot_solution\n",
    "\n",
    "_, ax = plt.subplots(figsize=(8, 8))\n",
    "plot_solution(res.best, m.data(), ax=ax)"
   ]
  },
  {
   "attachments": {},
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "We have just solved our first vehicle routing problem using PyVRP!"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    ".. warning::\n",
    "   PyVRP automatically converts all numeric input values to integers.\n",
    "   If your data has decimal values, you must scale and convert them to integers first to avoid unexpected behaviour.\n"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "### VRP with time windows\n",
    "\n",
    "\n",
    "Besides the capacitated VRP, PyVRP also supports the VRP with time windows.\n",
    "Let's see if we can also solve such an instance, again following the [OR-Tools documentation](https://developers.google.com/optimization/routing/vrptw).\n",
    "Like in the OR-Tools example, we will ignore capacity restrictions, and give each vehicle a maximum route duration of 30.\n",
    "Unlike the OR-Tools example, we still aim to minimise the total travel _distance_, not _duration_."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "# fmt: off\n",
    "DURATION_MATRIX = [\n",
    "        [0, 6, 9, 8, 7, 3, 6, 2, 3, 2, 6, 6, 4, 4, 5, 9, 7],\n",
    "        [6, 0, 8, 3, 2, 6, 8, 4, 8, 8, 13, 7, 5, 8, 12, 10, 14],\n",
    "        [9, 8, 0, 11, 10, 6, 3, 9, 5, 8, 4, 15, 14, 13, 9, 18, 9],\n",
    "        [8, 3, 11, 0, 1, 7, 10, 6, 10, 10, 14, 6, 7, 9, 14, 6, 16],\n",
    "        [7, 2, 10, 1, 0, 6, 9, 4, 8, 9, 13, 4, 6, 8, 12, 8, 14],\n",
    "        [3, 6, 6, 7, 6, 0, 2, 3, 2, 2, 7, 9, 7, 7, 6, 12, 8],\n",
    "        [6, 8, 3, 10, 9, 2, 0, 6, 2, 5, 4, 12, 10, 10, 6, 15, 5],\n",
    "        [2, 4, 9, 6, 4, 3, 6, 0, 4, 4, 8, 5, 4, 3, 7, 8, 10],\n",
    "        [3, 8, 5, 10, 8, 2, 2, 4, 0, 3, 4, 9, 8, 7, 3, 13, 6],\n",
    "        [2, 8, 8, 10, 9, 2, 5, 4, 3, 0, 4, 6, 5, 4, 3, 9, 5],\n",
    "        [6, 13, 4, 14, 13, 7, 4, 8, 4, 4, 0, 10, 9, 8, 4, 13, 4],\n",
    "        [6, 7, 15, 6, 4, 9, 12, 5, 9, 6, 10, 0, 1, 3, 7, 3, 10],\n",
    "        [4, 5, 14, 7, 6, 7, 10, 4, 8, 5, 9, 1, 0, 2, 6, 4, 8],\n",
    "        [4, 8, 13, 9, 8, 7, 10, 3, 7, 4, 8, 3, 2, 0, 4, 5, 6],\n",
    "        [5, 12, 9, 14, 12, 6, 6, 7, 3, 3, 4, 7, 6, 4, 0, 9, 2],\n",
    "        [9, 10, 18, 6, 8, 12, 15, 8, 13, 9, 13, 3, 4, 5, 9, 0, 9],\n",
    "        [7, 14, 9, 16, 14, 8, 5, 10, 6, 5, 4, 10, 8, 6, 2, 9, 0],\n",
    "]\n",
    "TIME_WINDOWS = [\n",
    "        (0, 999),  # location 0 - the depot (modified to be unrestricted)\n",
    "        (7, 12),   # location 1\n",
    "        (10, 15),  # location 2\n",
    "        (16, 18),  # location 3\n",
    "        (10, 13),  # location 4\n",
    "        (0, 5),    # location 5\n",
    "        (5, 10),   # location 6\n",
    "        (0, 4),    # location 7\n",
    "        (5, 10),   # location 8\n",
    "        (0, 3),    # location 9\n",
    "        (10, 16),  # location 10\n",
    "        (10, 15),  # location 11\n",
    "        (0, 5),    # location 12\n",
    "        (5, 10),   # location 13\n",
    "        (7, 8),    # location 14\n",
    "        (10, 15),  # location 15\n",
    "        (11, 15),  # location 16\n",
    "]\n",
    "# fmt: on"
   ]
  },
  {
   "attachments": {},
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "We now need to specify the time windows for all locations, and the duration of travelling along each edge."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "m = Model()\n",
    "m.add_vehicle_type(4, max_duration=30)\n",
    "depot = m.add_depot(\n",
    "    x=COORDS[0][0],\n",
    "    y=COORDS[0][1],\n",
    "    tw_early=TIME_WINDOWS[0][0],\n",
    "    tw_late=TIME_WINDOWS[0][1],\n",
    ")\n",
    "clients = [\n",
    "    m.add_client(\n",
    "        x=COORDS[idx][0],\n",
    "        y=COORDS[idx][1],\n",
    "        tw_early=TIME_WINDOWS[idx][0],\n",
    "        tw_late=TIME_WINDOWS[idx][1],\n",
    "    )\n",
    "    for idx in range(1, len(COORDS))\n",
    "]\n",
    "\n",
    "locations = [depot] + clients\n",
    "for frm_idx, frm in enumerate(locations):\n",
    "    for to_idx, to in enumerate(locations):\n",
    "        distance = abs(frm.x - to.x) + abs(frm.y - to.y)  # Manhattan\n",
    "        duration = DURATION_MATRIX[frm_idx][to_idx]\n",
    "        m.add_edge(frm, to, distance=distance, duration=duration)"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "res = m.solve(stop=MaxRuntime(1), display=False)  # one second\n",
    "print(res)"
   ]
  },
  {
   "attachments": {},
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Due to the hard time windows requirements, the total travel distance has increased slightly compared to our solution for the capacitated VRP.\n",
    "Let's have a look at the new solution."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "_, ax = plt.subplots(figsize=(8, 8))\n",
    "plot_solution(res.best, m.data(), ax=ax)"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "### Multi-depot VRP with time windows\n",
    "\n",
    "Let's now solve a VRP with multiple depots and time windows.\n",
    "We consider two depots, and two vehicles per depot that have to start and end their routes at their respective depot.\n",
    "For this, we will re-use some of the data from the VRPTW case, but change the time window data slightly: the first client now becomes the second depot."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "# fmt: off\n",
    "TIME_WINDOWS = [\n",
    "    (0, 999),  # location 0 - a depot (modified to be unrestricted)\n",
    "    (0, 999),  # location 1 - a depot (modified to be unrestricted)\n",
    "    (10, 15),  # location 2\n",
    "    (16, 18),  # location 3\n",
    "    (10, 13),  # location 4\n",
    "    (0, 5),    # location 5\n",
    "    (5, 10),   # location 6\n",
    "    (0, 4),    # location 7\n",
    "    (5, 10),   # location 8\n",
    "    (0, 3),    # location 9\n",
    "    (10, 16),  # location 10\n",
    "    (10, 15),  # location 11\n",
    "    (0, 5),    # location 12\n",
    "    (5, 10),   # location 13\n",
    "    (7, 8),    # location 14\n",
    "    (10, 15),  # location 15\n",
    "    (11, 15),  # location 16\n",
    "]\n",
    "# fmt: on"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "m = Model()\n",
    "depots = [\n",
    "    m.add_depot(\n",
    "        x=COORDS[idx][0],\n",
    "        y=COORDS[idx][1],\n",
    "        tw_early=TIME_WINDOWS[idx][0],\n",
    "        tw_late=TIME_WINDOWS[idx][1],\n",
    "    )\n",
    "    for idx in range(2)\n",
    "]\n",
    "\n",
    "for depot in depots:\n",
    "    # Two vehicles at each of the depots, with maximum route durations\n",
    "    # of 30.\n",
    "    m.add_vehicle_type(2, depot=depot, max_duration=30)\n",
    "\n",
    "clients = [\n",
    "    m.add_client(\n",
    "        x=COORDS[idx][0],\n",
    "        y=COORDS[idx][1],\n",
    "        tw_early=TIME_WINDOWS[idx][0],\n",
    "        tw_late=TIME_WINDOWS[idx][1],\n",
    "    )\n",
    "    for idx in range(2, len(COORDS))\n",
    "]\n",
    "\n",
    "locations = depots + clients\n",
    "for frm_idx, frm in enumerate(locations):\n",
    "    for to_idx, to in enumerate(locations):\n",
    "        distance = abs(frm.x - to.x) + abs(frm.y - to.y)  # Manhattan\n",
    "        duration = DURATION_MATRIX[frm_idx][to_idx]\n",
    "        m.add_edge(frm, to, distance=distance, duration=duration)"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Let's have a look at the modified data instance to familiarise ourself with the changes."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "_, ax = plt.subplots(figsize=(8, 8))\n",
    "plot_coordinates(m.data(), ax=ax)"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Let's solve the instance."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "res = m.solve(stop=MaxRuntime(1), display=False)  # one second\n",
    "print(res)"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "_, ax = plt.subplots(figsize=(8, 8))\n",
    "plot_solution(res.best, m.data(), ax=ax)"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "### Prize-collecting VRP\n",
    "\n",
    "We now have a basic familiarity with PyVRP's `Model` interface, but have not seen some of its additional features yet.\n",
    "In this short section we will discuss _optional_ clients, which offer a reward (a prize) when they are visited, but are not required for feasibility.\n",
    "This VRP variant is often called a prize-collecting VRP, and PyVRP supports this out-of-the-box.\n",
    "\n",
    "Let's stick to the multiple depot setting, and also define a `PRIZES` list that provides the prizes of visiting each client."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "# fmt: off\n",
    "PRIZES = [\n",
    "    0,    # location 0 - a depot\n",
    "    0,    # location 1 - a depot\n",
    "    334,  # location 2\n",
    "    413,  # location 3\n",
    "    295,  # location 4\n",
    "    471,  # location 5\n",
    "    399,  # location 6\n",
    "    484,  # location 7\n",
    "    369,  # location 8\n",
    "    410,  # location 9\n",
    "    471,  # location 10\n",
    "    382,  # location 11\n",
    "    347,  # location 12\n",
    "    380,  # location 13\n",
    "    409,  # location 14\n",
    "    302,  # location 15\n",
    "    411,  # location 16\n",
    "]\n",
    "# fmt: on"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "When modelling optional clients, it is important to provide both a reward (the `prize` argument to `add_client`), and to mark the client as optional by passing `required=False`:"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "m = Model()\n",
    "depots = [\n",
    "    m.add_depot(\n",
    "        x=COORDS[idx][0],\n",
    "        y=COORDS[idx][1],\n",
    "        tw_early=TIME_WINDOWS[idx][0],\n",
    "        tw_late=TIME_WINDOWS[idx][1],\n",
    "    )\n",
    "    for idx in range(2)\n",
    "]\n",
    "\n",
    "for depot in depots:\n",
    "    m.add_vehicle_type(2, depot=depot)\n",
    "\n",
    "clients = [\n",
    "    m.add_client(\n",
    "        x=COORDS[idx][0],\n",
    "        y=COORDS[idx][1],\n",
    "        tw_early=TIME_WINDOWS[idx][0],\n",
    "        tw_late=TIME_WINDOWS[idx][1],\n",
    "        prize=PRIZES[idx],\n",
    "        required=False,\n",
    "    )\n",
    "    for idx in range(2, len(COORDS))\n",
    "]\n",
    "\n",
    "locations = depots + clients\n",
    "for frm_idx, frm in enumerate(locations):\n",
    "    for to_idx, to in enumerate(locations):\n",
    "        distance = abs(frm.x - to.x) + abs(frm.y - to.y)  # Manhattan\n",
    "        duration = DURATION_MATRIX[frm_idx][to_idx]\n",
    "        m.add_edge(frm, to, distance=distance, duration=duration)"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "res = m.solve(stop=MaxRuntime(1), display=False)  # one second\n",
    "print(res)"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "_, ax = plt.subplots(figsize=(8, 8))\n",
    "plot_solution(res.best, m.data(), plot_clients=True, ax=ax)"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Some clients are not visited in the figure above.\n",
    "These clients are too far from other locations for their prizes to be worth the additional travel cost of visiting.\n",
    "Thus, PyVRP's solver opts not to visit such optional clients."
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "### VRP with simultaneous pickup and delivery\n",
    "\n",
    "Finally, we consider the VRP with simultaneous pickup and delivery.\n",
    "In this problem variant, clients request items from the depot, and also produce return shipments that needs to be delivered back to the depot after visiting the client.\n",
    "Thus, there are both deliveries from the depot to the clients, and pickups from the clients to the depot.\n",
    "\n",
    "Let's remain in the multi-depot, prize-collecting world we entered through the last example.\n",
    "We first define a `LOADS` list that tracks the delivery and pickup amount for each location:"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "# fmt: off\n",
    "LOADS = [\n",
    "    (0, 0),   # location 0 - a depot\n",
    "    (0, 0),   # location 1 - a depot\n",
    "    (1, 4),   # location 2 - simultaneous pickup and delivery\n",
    "    (2, 0),   # location 3 - pure delivery\n",
    "    (0, 5),   # location 4 - pure pickup\n",
    "    (6, 3),   # location 5 - simultaneous pickup and delivery\n",
    "    (4, 7),   # location 6 - simultaneous pickup and delivery\n",
    "    (11, 0),  # location 7 - pure delivery\n",
    "    (3, 0),   # location 8 - pure delivery\n",
    "    (0, 5),   # location 9 - pure pickup\n",
    "    (6, 4),   # location 10 - simultaneous pickup and delivery\n",
    "    (1, 4),   # location 11 - simultaneous pickup and delivery\n",
    "    (0, 3),   # location 12 - pure pickup\n",
    "    (6, 0),   # location 13 - pure delivery\n",
    "    (3, 2),   # location 14 - simultaneous pickup and delivery\n",
    "    (4, 3),   # location 15 - simultaneous pickup and delivery\n",
    "    (0, 6),   # location 16 - pure pickup\n",
    "]\n",
    "# fmt: on"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "m = Model()\n",
    "depots = [\n",
    "    m.add_depot(\n",
    "        x=COORDS[idx][0],\n",
    "        y=COORDS[idx][1],\n",
    "        tw_early=TIME_WINDOWS[idx][0],\n",
    "        tw_late=TIME_WINDOWS[idx][1],\n",
    "    )\n",
    "    for idx in range(2)\n",
    "]\n",
    "\n",
    "for depot in depots:\n",
    "    m.add_vehicle_type(2, depot=depot, capacity=15)\n",
    "\n",
    "clients = [\n",
    "    m.add_client(\n",
    "        x=COORDS[idx][0],\n",
    "        y=COORDS[idx][1],\n",
    "        tw_early=TIME_WINDOWS[idx][0],\n",
    "        tw_late=TIME_WINDOWS[idx][1],\n",
    "        delivery=LOADS[idx][0],\n",
    "        pickup=LOADS[idx][1],\n",
    "        prize=PRIZES[idx],\n",
    "        required=False,\n",
    "    )\n",
    "    for idx in range(2, len(COORDS))\n",
    "]\n",
    "\n",
    "locations = depots + clients\n",
    "for frm_idx, frm in enumerate(locations):\n",
    "    for to_idx, to in enumerate(locations):\n",
    "        distance = abs(frm.x - to.x) + abs(frm.y - to.y)  # Manhattan\n",
    "        duration = DURATION_MATRIX[frm_idx][to_idx]\n",
    "        m.add_edge(frm, to, distance=distance, duration=duration)"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "res = m.solve(stop=MaxRuntime(1), display=False)  # one second\n",
    "print(res)"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "_, ax = plt.subplots(figsize=(8, 8))\n",
    "plot_solution(res.best, m.data(), plot_clients=True, ax=ax)"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "This concludes the brief tutorial: you now know how to model and solve vehicle routing problems using PyVRP's `Model` interface.\n",
    "PyVRP supports several additional VRP variants we have not covered here.\n",
    "Have a look at the VRP introduction and other documentation pages to see how those can be modelled and solved. "
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": []
  }
 ],
 "metadata": {
  "kernelspec": {
   "display_name": ".venv",
   "language": "python",
   "name": "python3"
  },
  "language_info": {
   "codemirror_mode": {
    "name": "ipython",
    "version": 3
   },
   "file_extension": ".py",
   "mimetype": "text/x-python",
   "name": "python",
   "nbconvert_exporter": "python",
   "pygments_lexer": "ipython3",
   "version": "3.11.8"
  }
 },
 "nbformat": 4,
 "nbformat_minor": 2
}
